<!DOCTYPE html>
<html class="has-navbar-fixed-top">
<head>
    <meta charset="utf-8">
<title>IO - wanzixin</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/outdated-browser/1.1.5/outdatedbrowser.min.css">


<link href="/Study/Java/zh-cn/IO/" rel="alternate" hreflang="en" />
    


<meta name="description" content="">





    <meta name="description" content="I&#x2F;O是指Input&#x2F;Output，即输入和输出。以内存为中心，Input指从外部读入数据到内存，例如把文件从磁盘读取到内存，从网络读取数据到内存等等；Output指把数据从内存输出到外部，例如，把数据从内存写入到文件，把数据从内存输出到网络等等。">
<meta property="og:type" content="article">
<meta property="og:title" content="IO">
<meta property="og:url" content="https://wanzixin.github.io/Study/Java/zh-cn/IO/index.html">
<meta property="og:site_name" content="wanzixin">
<meta property="og:description" content="I&#x2F;O是指Input&#x2F;Output，即输入和输出。以内存为中心，Input指从外部读入数据到内存，例如把文件从磁盘读取到内存，从网络读取数据到内存等等；Output指把数据从内存输出到外部，例如，把数据从内存写入到文件，把数据从内存输出到网络等等。">
<meta property="og:locale" content="zh_CN">
<meta property="article:published_time" content="2021-05-07T01:29:23.000Z">
<meta property="article:modified_time" content="2021-05-30T13:32:14.220Z">
<meta property="article:author" content="wanzixin">
<meta name="twitter:card" content="summary">





<link rel="icon" href="/favicon.png">


<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Ovo|Source+Code+Pro">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">


<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/lightgallery/1.6.8/css/lightgallery.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.6.5/css/justifiedGallery.min.css">


<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/atom-one-light.min.css">


<link rel="stylesheet" href="/css/style.css">


<script defer src="//use.fontawesome.com/releases/v5.0.8/js/all.js"></script>


    
    
    
    
    
    
    
    
    
    

    


<meta name="generator" content="Hexo 5.4.0"></head>
<body>
    
<nav class="navbar is-transparent is-fixed-top navbar-main" role="navigation" aria-label="main navigation">
    <div class="container">
        <div class="navbar-brand">
            <a class="navbar-item navbar-logo" href="/zh-cn">
                
                    
                    wanzixin
                    
                
            </a>
            <div class="navbar-burger">
                <span></span>
                <span></span>
                <span></span>
            </div>
        </div>
        
        <div class="navbar-menu navbar-start">
            
            <a class="navbar-item "
               href="/archives">Archives</a>
            
            <a class="navbar-item "
               href="/categories">Categories</a>
            
            <a class="navbar-item "
               href="/categories/Diary">Diary</a>
            
            <a class="navbar-item "
               href="/categories/Gallery">Gallery</a>
            
            <a class="navbar-item "
               href="/categories/Study">Study</a>
            
            <a class="navbar-item "
               href="/categories/Item">Item</a>
            
            <a class="navbar-item "
               href="/about">About</a>
            
        </div>
        
        <div class="navbar-menu navbar-end">
            
            <a class="navbar-item search" title="搜索" href="javascript:;">
                <i class="fas fa-search"></i>
            </a>
            
            
            <div class="navbar-item is-hoverable has-dropdown is-hidden-mobile is-hidden-tablet-only toc">
                <a class="navbar-item" title="目录">
                    <i class="fa fa-list"></i>
                </a>
                <div class="navbar-dropdown is-right">
                    
                    
                    
                    
                    <a class="navbar-item" href="#IO流">1&nbsp;&nbsp;<b>IO流</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#InputStream-OutputStream">1.1&nbsp;&nbsp;InputStream/OutputStream</a>
                    
                    
                    
                    <a class="navbar-item" href="#Reader-Writer">1.2&nbsp;&nbsp;Reader/Writer</a>
                    
                    
                    
                    <a class="navbar-item" href="#同步和异步">1.3&nbsp;&nbsp;同步和异步</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#File对象">2&nbsp;&nbsp;<b>File对象</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#文件和目录">2.1&nbsp;&nbsp;文件和目录</a>
                    
                    
                    
                    <a class="navbar-item" href="#创建和删除文件">2.2&nbsp;&nbsp;创建和删除文件</a>
                    
                    
                    
                    <a class="navbar-item" href="#遍历文件和目录">2.3&nbsp;&nbsp;遍历文件和目录</a>
                    
                    
                    
                    <a class="navbar-item" href="#Path">2.4&nbsp;&nbsp;Path</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#InputStream">3&nbsp;&nbsp;<b>InputStream</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#缓冲">3.1&nbsp;&nbsp;缓冲</a>
                    
                    
                    
                    <a class="navbar-item" href="#阻塞">3.2&nbsp;&nbsp;阻塞</a>
                    
                    
                    
                    <a class="navbar-item" href="#InputStream实现类">3.3&nbsp;&nbsp;InputStream实现类</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#OutputStream">4&nbsp;&nbsp;<b>OutputStream</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#FileOutputStream">4.1&nbsp;&nbsp;FileOutputStream</a>
                    
                    
                    
                    <a class="navbar-item" href="#阻塞-1">4.2&nbsp;&nbsp;阻塞</a>
                    
                    
                    
                    <a class="navbar-item" href="#OutputStream实现类">4.3&nbsp;&nbsp;OutputStream实现类</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#Filter模式">5&nbsp;&nbsp;<b>Filter模式</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#编写FilterInputStream">5.1&nbsp;&nbsp;编写FilterInputStream</a>
                    
                    
                    
                    <a class="navbar-item" href="#小结">5.2&nbsp;&nbsp;小结</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#操作Zip">6&nbsp;&nbsp;<b>操作Zip</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#读取zip包">6.1&nbsp;&nbsp;读取zip包</a>
                    
                    
                    
                    <a class="navbar-item" href="#写入zip包">6.2&nbsp;&nbsp;写入zip包</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#读取classpath资源">7&nbsp;&nbsp;<b>读取classpath资源</b></a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#序列化">8&nbsp;&nbsp;<b>序列化</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#序列化-1">8.1&nbsp;&nbsp;序列化</a>
                    
                    
                    
                    <a class="navbar-item" href="#反序列化">8.2&nbsp;&nbsp;反序列化</a>
                    
                    
                    
                    <a class="navbar-item" href="#安全性">8.3&nbsp;&nbsp;安全性</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#Reader">9&nbsp;&nbsp;<b>Reader</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#FileReader">9.1&nbsp;&nbsp;FileReader</a>
                    
                    
                    
                    <a class="navbar-item" href="#CharArrayReader">9.2&nbsp;&nbsp;CharArrayReader</a>
                    
                    
                    
                    <a class="navbar-item" href="#StringReader">9.3&nbsp;&nbsp;StringReader</a>
                    
                    
                    
                    <a class="navbar-item" href="#InputStreamReader">9.4&nbsp;&nbsp;InputStreamReader</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#PrintStream和PrintWriter">10&nbsp;&nbsp;<b>PrintStream和PrintWriter</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#PrintWriter">10.1&nbsp;&nbsp;PrintWriter</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#使用Files">11&nbsp;&nbsp;<b>使用Files</b></a>
                    
                </div>
            </div>
            
            
            <a class="navbar-item" title="GitHub" target="_blank" rel="noopener" href="https://github.com/wanzixin">
                
                <i class="fab fa-github"></i>
                
            </a>
               
            
        </div>
    </div>
</nav>

    <section class="section">
    <div class="container">
    <article class="article content gallery" itemscope itemprop="blogPost">
    <h1 class="article-title is-size-3 is-size-4-mobile" itemprop="name">
        
            IO
        
    </h1>
    <div class="article-meta columns is-variable is-1 is-multiline is-mobile is-size-7-mobile">
        <span class="column is-narrow">
            
                <span>5月 7 2021</span>
            
        </span>
        
        <span class="column is-narrow article-category">
            <i class="far fa-folder"></i>
            <a class="article-category-link" href="/zh-cn/categories/Study/">Study</a><span>></span><a class="article-category-link" href="/zh-cn/categories/Study/Java/">Java</a>
        </span>
        
        
        <span class="column is-narrow">
            
            
            1 小时 读完 (约 10464 字)
        </span>
        
    </div>
    <div class="article-entry is-size-6-mobile" itemprop="articleBody">
    
        <html><head></head><body><p>I/O是指Input/Output，即输入和输出。以内存为中心，Input指从外部读入数据到内存，例如把文件从磁盘读取到内存，从网络读取数据到内存等等；Output指把数据从内存输出到外部，例如，把数据从内存写入到文件，把数据从内存输出到网络等等。<span id="more"></span>为什么要把数据读到内存才能处理这些数据？因为代码是在内存中运行的，数据也必须读到内存，最终的表示方式无非是byte数组，字符串等，都必须存放在内存里。</p>
<p>从Java代码来看，输入实际上就是从外部，例如，硬盘上的某个文件，把内容读到内存，并且以Java提供的某种数据类型表示，例如，byte[]，String，这样后续代码才能处理这些数据。因为内存有“易失性”，所以必须把处理后的数据以某种方式输出，例如，写入到文件。Output实际上就是把Java表示的数据格式，例如，byte[]，String等输出到某个地方。</p>
<h2 id="IO流"><a href="#IO流" class="headerlink" title="IO流"></a>IO流</h2><p>IO流是一种顺序读写数据的模式，它的特点是单向流动。数据类似自来水一样在水管中流动，所以我们把它称为IO流。</p>
<h3 id="InputStream-OutputStream"><a href="#InputStream-OutputStream" class="headerlink" title="InputStream/OutputStream"></a>InputStream/OutputStream</h3><p>IO流以byte（字节）为最小单位，因此也称为字节流。例如，我们要从磁盘读入一个文件，包含6个字节，就相当于读入了6个字节的数据。这6个字节是按顺序读入的，所以是输入字节流。反过来，我们把6个字节从内存写入磁盘文件，就是输出字节流。在Java中，InputStream代表输入字节流，OutputStream代表输出字节流，这是最基本的两种IO流。</p>
<h3 id="Reader-Writer"><a href="#Reader-Writer" class="headerlink" title="Reader/Writer"></a>Reader/Writer</h3><p>如果我们需要读写的是字符，并且字符不全是单字节表示的ASCII字符，那么，按照char来读写显然更方便，这种流称为字符流。Java提供了Reader和Writer表示字符流，字符流传输的最小数据单位是char。例如，我们把char[]数组<code>Hi你好</code>这4个字符用Writer字符流写入文件，并且使用UTF-8编码，得到的最终文件内容是8个字节，英文字符<code>H</code>和<code>i</code>各占一个字节，中文字符<code>你好</code>各占3个字节。</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs utf-8">0x48<br>0x69<br>0xe4bda0<br>0xe5a5bd<br></code></pre></td></tr></tbody></table></figure>

<p>反过来，我们用Reader读取以UTF-8编码的这8个字节，会从Reader中得到<code>Hi你好</code>这4个字符。</p>
<p>因此，Reader和Writer本质上是一个能自动编码解码的InputStream和OutputStream。</p>
<p>使用Reader，数据源虽然是字节，但我们读入的数据都是char类型的字符，原因是Reader内部把读入的byte做了解码，转换成了char。使用InputStream，我们读入的数据和原始二进制数据一模一样，是byte[]数组，但是我们可以自己把二进制byte[]数组按照某种编码转换成字符串。究竟使用Reader还是InputStream，要取决于具体的使用场景。如果数据源不是文本，就只能使用InputStream，如果数据源是文本，使用Reader更方便一些。Writer和OutputStream是类似的。</p>
<h3 id="同步和异步"><a href="#同步和异步" class="headerlink" title="同步和异步"></a>同步和异步</h3><p>同步IO是指，读写IO时代码必须等待数据返回后才继续执行后续代码，它的优点是代码编写简单，缺点是CPU执行效率低。而异步IO是指，读写IO时仅发出请求，然后立刻执行后续代码，它的优点是CPU执行效率高，缺点是代码编写复杂。</p>
<p>Java标准库的包java.io提供了同步IO，而java.nio则是异步IO。上面我们讨论的InputStream、OutputStream、Reader和Writer都是同步IO的抽象类，对应的具体实现类，以文件为例，有FileInputStream、FileOutputSteam、FileReader和FileWriter。</p>
<p>本节我们只讨论Java的同步IO，即输入/输出流的IO模型。</p>
<h2 id="File对象"><a href="#File对象" class="headerlink" title="File对象"></a>File对象</h2><p>在计算机系统中，文件是非常重要的存储方式。Java的标准库<code>java.io</code>提供了File对象来操作文件和目录。</p>
<p>要构造一个File对象，需要传入文件路径，既可以传入绝对路径，也可以传入相对路径。绝对路径是以根目录开头的完整路径，例如：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">File f = <span class="hljs-keyword">new</span> File(<span class="hljs-string">"C:\\Windows\\notepad.exe"</span>);<br></code></pre></td></tr></tbody></table></figure>

<p>传入相对路径时，相对路径前面加上当前目录就是绝对路径。可以用<code>.</code>来表示当前路径，<code>..</code>用来表示上级目录。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// 假设当前目录是C:\Docs</span><br>File f1 = <span class="hljs-keyword">new</span> File(<span class="hljs-string">"sub\\javac"</span>); <span class="hljs-comment">// 绝对路径是C:\Docs\sub\javac</span><br>File f3 = <span class="hljs-keyword">new</span> File(<span class="hljs-string">".\\sub\\javac"</span>); <span class="hljs-comment">// 绝对路径是C:\Docs\sub\javac</span><br>File f3 = <span class="hljs-keyword">new</span> File(<span class="hljs-string">"..\\sub\\javac"</span>); <span class="hljs-comment">// 绝对路径是C:\sub\javac</span><br></code></pre></td></tr></tbody></table></figure>

<p>File对象有3种形式表示的路径，一种是getPath()，返回构造方法传入的路径，一种是getAbsolutePath()，返回绝对路径，一种是getCanonicalPath()，它和绝对路径相似，但返回的是规范路径。</p>
<p>什么是规范路径呢？例如，绝对路径可以表示成<code>C:\Windows\System32\..\notepad.exe</code>，而规范路径就是把<code>.</code>和<code>..</code>转换成标准的绝对路径后的路径：<code>C:\Windows\notepad.exe</code>。Windows和Linux的路径分隔符不同，File对象有一个静态变量用于表示当前平台的系统分隔符。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">System.out.println(File.separator); <span class="hljs-comment">// 根据当前平台打印"\"或"/"</span><br></code></pre></td></tr></tbody></table></figure>

<h3 id="文件和目录"><a href="#文件和目录" class="headerlink" title="文件和目录"></a>文件和目录</h3><p>File对象既可以表示文件，也可以表示目录。特别要注意的是，构造一个File对象，即使传入的文件或目录不存在，代码也不会出错，因为构造一个File对象，并不会导致任何磁盘操作。只有当我们调用File对象的某些方法时，才真正进行磁盘操作。例如，调用<code>isFile()</code>判断一个File对象是否是一个已存在的文件，调用<code>isDirectory()</code>判断该File对象是否是一个已存在的目录。</p>
<p>用File对象获取到一个文件时，还可以进一步判断文件的权限和大小：</p>
<ul>
<li>boolean canRead()：是否可读；</li>
<li>boolean canWrite()：是否可写；</li>
<li>boolean canExecute()：是否可执行；</li>
<li>long length()：文件字节大小。</li>
</ul>
<p>对目录而言，是否可执行表示能否列出它包含的文件和子目录。</p>
<h3 id="创建和删除文件"><a href="#创建和删除文件" class="headerlink" title="创建和删除文件"></a>创建和删除文件</h3><p>当File对象表示一个文件时，可以通过<code>createNewFile()</code>创建一个新文件，用<code>delete()</code>删除该文件。</p>
<p>有时候，程序需要读取一些临时文件，File对象提供了<code>createTempFile()</code>来创建一个临时文件，以及<code>deleteOnExit()</code>在JVM退出时自动删除该文件。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs java">File f = File.createTempFile(<span class="hljs-string">"tmp-"</span>, <span class="hljs-string">".txt"</span>); <span class="hljs-comment">// 提供临时文件的前缀和后缀</span><br>f.deleteOnExit(); <span class="hljs-comment">// JVM退出时自动删除</span><br></code></pre></td></tr></tbody></table></figure>

<h3 id="遍历文件和目录"><a href="#遍历文件和目录" class="headerlink" title="遍历文件和目录"></a>遍历文件和目录</h3><p>当File对象表示一个目录时，可以使用<code>list()</code>和<code>listFiles()</code>列出目录下的文件和子目录名。ListFiles()提供了一系列重载方法，可以过滤不想要的文件和目录。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>    File f = <span class="hljs-keyword">new</span> File(<span class="hljs-string">"C:\\Windows"</span>);<br>    File[] fs1 = f.listFiles(); <span class="hljs-comment">// 列出所有文件和子目录</span><br>    printFiles(fs1);<br>    File[] fs2 = f.listFiles(<span class="hljs-keyword">new</span> FilenameFilter() { <span class="hljs-comment">// 仅列出.exe文件</span><br>        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">accept</span><span class="hljs-params">(File dir, String name)</span> </span>{<br>            <span class="hljs-keyword">return</span> name.endsWith(<span class="hljs-string">".exe"</span>); <span class="hljs-comment">// 返回true表示接受该文件</span><br>        }<br>    });<br>    printFiles(fs2);<br>}<br><br><span class="hljs-function"><span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">printFiles</span><span class="hljs-params">(File[] files)</span> </span>{<br>    System.out.println(<span class="hljs-string">"=========="</span>);<br>    <span class="hljs-keyword">if</span> (files != <span class="hljs-keyword">null</span>) {<br>        <span class="hljs-keyword">for</span> (File f : files) {<br>            System.out.println(f);<br>        }<br>    }<br>    System.out.println(<span class="hljs-string">"=========="</span>);<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>和文件操作类似，File对象如果表示一个目录，可以通过以下方法创建和删除目录：</p>
<ul>
<li>boolean mkdir()：创建当前File对象表示的目录</li>
<li>boolean mkdirs()：创建当前File对象表示的目录，并在必要时将不存在的父目录也创建出来</li>
<li>boolean delete()：删除当前File对象表示的目录，当前目录必须为空才能删除成功</li>
</ul>
<h3 id="Path"><a href="#Path" class="headerlink" title="Path"></a>Path</h3><p>Java标准库还提供了一个Path对象，它位于<code>java.nio.file</code>包。Path对象和File对象类似，但操作更加简单。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> java.io.*;<br><span class="hljs-keyword">import</span> java.nio.file.*;<br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Main</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        Path p1 = Paths.get(<span class="hljs-string">"."</span>, <span class="hljs-string">"project"</span>, <span class="hljs-string">"study"</span>); <span class="hljs-comment">// 构造一个Path对象</span><br>        System.out.println(p1);<br>        Path p2 = p1.toAbsolutePath(); <span class="hljs-comment">// 转换为绝对路径</span><br>        System.out.println(p2);<br>        Path p3 = p2.normalize(); <span class="hljs-comment">// 转换为规范路径</span><br>        System.out.println(p3);<br>        File f = p3.toFile(); <span class="hljs-comment">// 转换为File对象</span><br>        System.out.println(f);<br>        <span class="hljs-keyword">for</span> (Path p : Paths.get(<span class="hljs-string">".."</span>).toAbsolutePath()) { <span class="hljs-comment">// 可以直接遍历Path</span><br>            System.out.println(<span class="hljs-string">"  "</span> + p);<br>        }<br>    }<br>}<br><br><span class="hljs-comment">/*打印出来以下内容*/</span><br>./project/study<br>/app/./project/study<br>/app/project/study<br>/app/project/study<br>  app<br>  ..<br></code></pre></td></tr></tbody></table></figure>

<p>如果需要对目录进行复杂的拼接、遍历等操作，使用Path对象更方便。</p>
<h2 id="InputStream"><a href="#InputStream" class="headerlink" title="InputStream"></a>InputStream</h2><p>InputStream就是Java标准库提供的最基本的输入流。它位于java.io这个包里。java.io包提供了所有同步IO的功能。要特别注意的一点是，InputStream并不是一个接口，而是一个抽象类，它是所有输入流的超类。这个抽象类定义的一个最重要的方法是<code>int read()</code>，签名如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">int</span> <span class="hljs-title">read</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException</span>;<br></code></pre></td></tr></tbody></table></figure>

<p>这个方法会读取输入流的下一个字节，并返回字节表示的int值（0~255）。如果已读到末尾，返回-1表示不能继续读取了。</p>
<p>FileInputStream是InputStream的一个子类。顾名思义，FileInputStream就是从文件流种读取数据。下面的代码演示了如何完整的读取一个FileInputStream的所有字节：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">readFile</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>    <span class="hljs-comment">// 创建一个FileInputStream对象:</span><br>    InputStream input = <span class="hljs-keyword">new</span> FileInputStream(<span class="hljs-string">"src/readme.txt"</span>);<br>    <span class="hljs-keyword">for</span> (;;) {<br>        <span class="hljs-keyword">int</span> n = input.read(); <span class="hljs-comment">// 反复调用read()方法，直到返回-1</span><br>        <span class="hljs-keyword">if</span> (n == -<span class="hljs-number">1</span>) {<br>            <span class="hljs-keyword">break</span>;<br>        }<br>        System.out.println(n); <span class="hljs-comment">// 打印byte的值</span><br>    }<br>    input.close(); <span class="hljs-comment">// 关闭流</span><br>}<br></code></pre></td></tr></tbody></table></figure>

<p>在计算机中，类似文件、网络端口这些资源，都是由操作系统统一管理的。应用程序在运行的过程中，如果打开了一个文件进行读写，完成后要及时的关闭，以便让操作系统把资源释放掉，否则，应用程序占用的资源会越来越多，不但白白占用内存，还会影响其他应用程序的运行。</p>
<p>InputStream和OutputStream都是通过close()方法来关闭流。关闭流就会释放对应的底层资源。</p>
<p>我们还注意到在读取或写入IO流的过程中，可能会发生错误，例如，文件不存在导致无法读取，没有写权限导致写入失败等等。这些底层错误由Java虚拟机自动封装成IOException异常并抛出。因此，所有与IO操作相关的代码都必须正确处理IOException。</p>
<p>仔细观察上面的代码，会发现一个潜在的问题：如果读取过程发生了IO错误，InputStream就没法正确的关闭，资源也就没法及时释放。因此，需要用try…finally来保证InputStream在无论是否发生IO错误的时候都能正确地关闭：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">readFile</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>    InputStream input = <span class="hljs-keyword">null</span>;<br>    <span class="hljs-keyword">try</span> {<br>        input = <span class="hljs-keyword">new</span> FileInputStream(<span class="hljs-string">"src/readme.txt"</span>);<br>        <span class="hljs-keyword">int</span> n;<br>        <span class="hljs-keyword">while</span> ((n = input.read()) != -<span class="hljs-number">1</span>) { <span class="hljs-comment">// 利用while同时读取并判断</span><br>            System.out.println(n);<br>        }<br>    } <span class="hljs-keyword">finally</span> {<br>        <span class="hljs-keyword">if</span> (input != <span class="hljs-keyword">null</span>) { input.close(); }<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>用try…finally来编写上述代码会感觉比较复杂，更好的写法是利用Java 7引入的新的<code>try(resource)</code>的语法，只需要编写try语句，让编译器自动为我们关闭资源。推荐的写法如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">readFile</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>    <span class="hljs-keyword">try</span> (InputStream input = <span class="hljs-keyword">new</span> FileInputStream(<span class="hljs-string">"src/readme.txt"</span>)) {<br>        <span class="hljs-keyword">int</span> n;<br>        <span class="hljs-keyword">while</span> ((n = input.read()) != -<span class="hljs-number">1</span>) {<br>            System.out.println(n);<br>        }<br>    } <span class="hljs-comment">// 编译器在此自动为我们写入finally并调用close()</span><br>}<br></code></pre></td></tr></tbody></table></figure>

<p>实际上，编译器并不会特别地为InputStream加上自动关闭。编译器只看<code>try(resource = ...)</code>中的对象是否实现了<code>java.lang.AutoCloseable</code>接口，如果实现了，就自动加上finally语句并调用close()方法。InputStream和OutputStream都实现了这个接口，因此，都可以用在<code>try(resource)</code>中。</p>
<h3 id="缓冲"><a href="#缓冲" class="headerlink" title="缓冲"></a>缓冲</h3><p>在读取流的时候，一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区，对于文件和网络流来说，利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节：</p>
<ul>
<li>int read(byte[] b)，读取若干字节并填充到byte[]数组，返回读取的字节数</li>
<li>int read(byte[] b, int off, int len)，指定byte[]数组的偏移量和最大填充数</li>
</ul>
<p>利用上述方法一次性读取多个字节时，需要先定义一个byte[]数组作为缓冲区，read()方法会尽可能多地读取字节到缓冲区，但不会超过缓冲区大小。read()方法的返回值不再是字节的int值，而是返回实际读取了多少个字节，如果返回-1，表示没有更多的数据了。利用缓冲区一次读取多个字节的代码如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">readFile</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>    <span class="hljs-keyword">try</span> (InputStream input = <span class="hljs-keyword">new</span> FileInputStream(<span class="hljs-string">"src/readme.txt"</span>)) {<br>        <span class="hljs-comment">// 定义1000个字节大小的缓冲区:</span><br>        <span class="hljs-keyword">byte</span>[] buffer = <span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[<span class="hljs-number">1000</span>];<br>        <span class="hljs-keyword">int</span> n;<br>        <span class="hljs-keyword">while</span> ((n = input.read(buffer)) != -<span class="hljs-number">1</span>) { <span class="hljs-comment">// 读取到缓冲区</span><br>            System.out.println(<span class="hljs-string">"read "</span> + n + <span class="hljs-string">" bytes."</span>);<br>        }<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<h3 id="阻塞"><a href="#阻塞" class="headerlink" title="阻塞"></a>阻塞</h3><p>在调用InputStream的read()方法读取数据时，我们说read()方法是阻塞（Blocking）的。它的意思是，对于下面的代码：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">int</span> n;<br>n = input.read(); <span class="hljs-comment">// 必须等待read()方法返回才能执行下一行代码</span><br><span class="hljs-keyword">int</span> m = n;<br></code></pre></td></tr></tbody></table></figure>

<p>执行到第二行代码时，必须等read()方法返回后才能继续。因为读取IO流相比执行普通代码，速度会慢很多，因此，无法确定read()方法调用到底要花费多少时间。</p>
<h3 id="InputStream实现类"><a href="#InputStream实现类" class="headerlink" title="InputStream实现类"></a>InputStream实现类</h3><p>用FileInputStream可以从文件获取输入流，这是InputStream常用的一个实现类。此外，ByteArrayInputStream可以在内存中模拟一个InputStream。ByteArrayInputStream实际上是把一个byte[]数组在内存中变成一个InputStream，虽然实际应用不多，但测试的时候，可以用它来构造一个InputStream。</p>
<h2 id="OutputStream"><a href="#OutputStream" class="headerlink" title="OutputStream"></a>OutputStream</h2><p>和InputStream相反，OutputStream是Java标准库提供的最基本的输出流。OutputStream也是抽象类，它是所有输出流的超类。这个抽象类定义的一个重要方法就是<code>void write(int b)</code>，签名如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">void</span> <span class="hljs-title">write</span><span class="hljs-params">(<span class="hljs-keyword">int</span> b)</span> <span class="hljs-keyword">throws</span> IOException</span>;<br></code></pre></td></tr></tbody></table></figure>

<p>这个方法会写入一个字节到输出流，要注意的是，虽然传入的是int参数，但只会写入一个字节，即只写入int表示低8位表示字节的部分，相当于b &amp; 0xff。</p>
<p>和InputStream类似，OutputStream也提供了close()方法关闭输出流，以便释放系统资源。要特别注意：OutputStream还提供了一个flush()方法，它的目的是将缓冲区的内容真正输出到目的地。为什么要有flush()？因为向磁盘、网络写入数据的时候，出于效率的考虑，操作系统并不是输出一个字节就立刻写入到文件或者发送到网络，而是把输出的字节先放到内存的一个缓冲区里（本质上就是一个byte[]数组），等到缓冲区写满了，再一次性写入文件或者网络。对于很多IO设备来说，一次写1个字节和一次写1000个字节，花费的时间几乎是一样的，所以OutputStream有个flush()方法，能强制把缓冲区内容输出。</p>
<p>通常情况下，我们不需要调用这个flush()方法，因为缓冲区写满后OutputStream会自动调用它，并且，在调用close()方法关闭OutputStream之前，也会自动调用flush()方法。</p>
<p>但是，在某些情况下，我们必须手动调用flush()方法。</p>
<blockquote>
<p>举个栗子：</p>
<p>小明正在开发一款在线聊天软件，当用户输入一句话后，就通过OutputStream的write()方法写入网络流。小明测试的时候发现，发送方输入后，接收方根本收不到任何信息，怎么肥四？</p>
<p>原因就在于写入网络流是先写入内存缓冲区，等缓冲区满了才会一次性发送到网络。如果缓冲区大小是4K，则发送方要敲几千个字符后，操作系统才会把缓冲区的内容发送出去，这个时候，接收方会一次性收到大量消息。</p>
<p>解决办法就是每输入一句话后，立刻调用flush()，不管当前缓冲区是否已满，强迫操作系统把缓冲区的内容立刻发送出去。</p>
</blockquote>
<p>实际上，InputStream也有缓冲区。例如，从FileInputStream读取一个字节时，操作系统往往会一次性读取若干字节到缓冲区，并维护一个指针指向未读的缓冲区。然后，每次我们调用int read()读取下一个字节时，可以直接返回缓冲区的下一字节，避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read()，则会出发操作系统的下一次读取并再次填满缓冲区。</p>
<h3 id="FileOutputStream"><a href="#FileOutputStream" class="headerlink" title="FileOutputStream"></a>FileOutputStream</h3><p>我们以FileOutputStream为例，演示如何将若干个字节写入文件流：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">writeFile</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>    OutputStream output = <span class="hljs-keyword">new</span> FileOutputStream(<span class="hljs-string">"out/readme.txt"</span>);<br>    output.write(<span class="hljs-number">72</span>); <span class="hljs-comment">// H</span><br>    output.write(<span class="hljs-number">101</span>); <span class="hljs-comment">// e</span><br>    output.write(<span class="hljs-number">108</span>); <span class="hljs-comment">// l</span><br>    output.write(<span class="hljs-number">108</span>); <span class="hljs-comment">// l</span><br>    output.write(<span class="hljs-number">111</span>); <span class="hljs-comment">// o</span><br>    output.close();<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>每次写入一个字节非常麻烦，更常见的方法是一次性写入若干字节。这是，可以用OutputStream提供的重载方法<code>void write(byte[])</code>来实现：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">writeFile</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>    OutputStream output = <span class="hljs-keyword">new</span> FileOutputStream(<span class="hljs-string">"out/readme.txt"</span>);<br>    output.write(<span class="hljs-string">"Hello"</span>.getBytes(<span class="hljs-string">"UTF-8"</span>)); <span class="hljs-comment">// Hello</span><br>    output.close();<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>和InputStream一样，上述代码没有考虑到在发生异常的情况下如何正确地关闭资源。写入过程也会经常发生IO错误，例如，磁盘已满，无权限写入等等。我们需要用try(resource)来保证OutputStream在无论是否发生IO错误的时候都能够正确地关闭：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">writeFile</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>    <span class="hljs-keyword">try</span> (OutputStream output = <span class="hljs-keyword">new</span> FileOutputStream(<span class="hljs-string">"out/readme.txt"</span>)) {<br>        output.write(<span class="hljs-string">"Hello"</span>.getBytes(<span class="hljs-string">"UTF-8"</span>)); <span class="hljs-comment">// Hello</span><br>    } <span class="hljs-comment">// 编译器在此自动为我们写入finally并调用close()</span><br>}<br></code></pre></td></tr></tbody></table></figure>

<h3 id="阻塞-1"><a href="#阻塞-1" class="headerlink" title="阻塞"></a>阻塞</h3><p>OutputStream的write()方法也是阻塞的。</p>
<h3 id="OutputStream实现类"><a href="#OutputStream实现类" class="headerlink" title="OutputStream实现类"></a>OutputStream实现类</h3><p>用FileOutputStream可以从文件获取输出流，这是OutputStream常用的一个实现类。此外，ByteArrayOutputStream可以在内存中模拟一个OutputStream。ByteArrayOutputStream实际上是把一个byte[]数组在内存中变成一个OutputStream，虽然实际应用不多，但测试的时候，可以用它来构造一个OutputStream。</p>
<p>同时操作多个<code>AutoCloseable</code>资源时，在<code>try(resource) { ... }</code>语句中可以同时写出多个资源，用<code>;</code>隔开。例如，同时读写两个文件：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// 读取input.txt，写入output.txt:</span><br><span class="hljs-keyword">try</span> (InputStream input = <span class="hljs-keyword">new</span> FileInputStream(<span class="hljs-string">"input.txt"</span>);<br>     OutputStream output = <span class="hljs-keyword">new</span> FileOutputStream(<span class="hljs-string">"output.txt"</span>))<br>{<br>    input.transferTo(output); <span class="hljs-comment">// transferTo的作用是?</span><br>}<br></code></pre></td></tr></tbody></table></figure>

<h2 id="Filter模式"><a href="#Filter模式" class="headerlink" title="Filter模式"></a>Filter模式</h2><p>Java的IO标准库提供的InputStream根据来源可以包括：</p>
<ul>
<li>FileInputStream，从文件读取数据，是最终数据源</li>
<li>ServletInputStream，从HTTP请求读取数据，是最终数据源</li>
<li>Socket.getInputStream()，从TCP连接读取数据，是最终数据源</li>
<li>…</li>
</ul>
<p>直接使用继承，为各种InputStream附加更多功能，根本无法控制代码的复杂度，很快就会失控。为了解决依赖继承会导致子类数量失控的问题，JDK首先讲InputStream分为两大类：</p>
<p>一类是直接提供数据的基础InputStream：</p>
<ul>
<li>FileInputStream</li>
<li>ByteArrayInputStream</li>
<li>ServletInputStream</li>
<li>…</li>
</ul>
<p>一类是提供附加功能的InputStream：</p>
<ul>
<li>BufferedInputStream，缓冲</li>
<li>DigestInputStream，计算签名</li>
<li>CipherInputStream，加密/解密</li>
<li>…</li>
</ul>
<p>当我们需要给一个基本的InputStream附加各种功能时，我们先确定这个能提供数据源的InputStream，因为我们需要的数据总得来自某个地方，例如，FileInputStream，数据来自文件：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">InputStream file = <span class="hljs-keyword">new</span> FileInputStream(<span class="hljs-string">"test.gz"</span>);<br></code></pre></td></tr></tbody></table></figure>

<p>紧接着，我们希望FileInputStream能提供缓冲的功能来提高读取效率，因此我们用BufferedInputStream包装这个InputStream，得到的包装类型是BufferedInputStream，但它仍然被视为一个InputStream：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">InputStream buffered = <span class="hljs-keyword">new</span> BufferedInputStream(file);<br></code></pre></td></tr></tbody></table></figure>

<p>最后，假设该文件已经用gzip压缩，我们希望直接读取解压缩的内容，就可以再包装一个GZIPInputStream：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">InputStream gzip = <span class="hljs-keyword">new</span> GZIPInputStream(buffered);<br></code></pre></td></tr></tbody></table></figure>

<p>无论我们包装多少次，得到的对象始终是InputStream，我们直接用InputStream来引用它，就可以正确读取。</p>
<p>上述这种通过一个“基础”组件再叠加各种“附加”功能组件的模式，称之为Filter模式（或者装饰器模式：Decorator）。它可以让我们通过少量的类来实现各种功能的组合：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs ascii">                 ┌─────────────┐<br>                 │ InputStream │<br>                 └─────────────┘<br>                       ▲ ▲<br>┌────────────────────┐ │ │ ┌─────────────────┐<br>│  FileInputStream   │─┤ └─│FilterInputStream│<br>└────────────────────┘ │   └─────────────────┘<br>┌────────────────────┐ │     ▲ ┌───────────────────┐<br>│ByteArrayInputStream│─┤     ├─│BufferedInputStream│<br>└────────────────────┘ │     │ └───────────────────┘<br>┌────────────────────┐ │     │ ┌───────────────────┐<br>│ ServletInputStream │─┘     ├─│  DataInputStream  │<br>└────────────────────┘       │ └───────────────────┘<br>                             │ ┌───────────────────┐<br>                             └─│CheckedInputStream │<br>                               └───────────────────┘<br></code></pre></td></tr></tbody></table></figure>

<p>类似的，OutputStream也是以这种模式来提供各种功能：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs ascii">                  ┌─────────────┐<br>                  │OutputStream │<br>                  └─────────────┘<br>                        ▲ ▲<br>┌─────────────────────┐ │ │ ┌──────────────────┐<br>│  FileOutputStream   │─┤ └─│FilterOutputStream│<br>└─────────────────────┘ │   └──────────────────┘<br>┌─────────────────────┐ │     ▲ ┌────────────────────┐<br>│ByteArrayOutputStream│─┤     ├─│BufferedOutputStream│<br>└─────────────────────┘ │     │ └────────────────────┘<br>┌─────────────────────┐ │     │ ┌────────────────────┐<br>│ ServletOutputStream │─┘     ├─│  DataOutputStream  │<br>└─────────────────────┘       │ └────────────────────┘<br>                              │ ┌────────────────────┐<br>                              └─│CheckedOutputStream │<br>                                └────────────────────┘<br></code></pre></td></tr></tbody></table></figure>

<h3 id="编写FilterInputStream"><a href="#编写FilterInputStream" class="headerlink" title="编写FilterInputStream"></a>编写FilterInputStream</h3><p>我们也可以自己编写FilterInputStream，以便可以把自己的FilterInputStream“叠加”到任何一个InputStream中。下面的例子演示了如何编写一个CountInputStream，它的作用是对输入的字节计数。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> java.io.*;<br><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Main</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        <span class="hljs-keyword">byte</span>[] data = <span class="hljs-string">"hello, world!"</span>.getBytes(<span class="hljs-string">"UTF-8"</span>);<br>        <span class="hljs-keyword">try</span> (CountInputStream input = <span class="hljs-keyword">new</span> CountInputStream(<span class="hljs-keyword">new</span> ByteArrayInputStream(data))) {<br>            <span class="hljs-keyword">int</span> n;<br>            <span class="hljs-keyword">while</span> ((n = input.read()) != -<span class="hljs-number">1</span>) {<br>                System.out.println((<span class="hljs-keyword">char</span>)n);<br>            }<br>            System.out.println(<span class="hljs-string">"Total read "</span> + input.getBytesRead() + <span class="hljs-string">" bytes"</span>);<br>        }<br>    }<br>}<br><br><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CountInputStream</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">FilterInputStream</span> </span>{<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> count = <span class="hljs-number">0</span>;<br><br>    CountInputStream(InputStream in) {<br>        <span class="hljs-keyword">super</span>(in);<br>    }<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">getBytesRead</span><span class="hljs-params">()</span> </span>{<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.count;<br>    }<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">read</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        <span class="hljs-keyword">int</span> n = in.read();<br>        <span class="hljs-keyword">if</span> (n != -<span class="hljs-number">1</span>) {<br>            <span class="hljs-keyword">this</span>.count ++;<br>        }<br>        <span class="hljs-keyword">return</span> n;<br>    }<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">read</span><span class="hljs-params">(<span class="hljs-keyword">byte</span>[] b, <span class="hljs-keyword">int</span> off, <span class="hljs-keyword">int</span> len)</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        <span class="hljs-keyword">int</span> n = in.read(b, off, len);<br>        <span class="hljs-keyword">if</span> (n != -<span class="hljs-number">1</span>) {<br>            <span class="hljs-keyword">this</span>.count += n;<br>        }<br>        <span class="hljs-keyword">return</span> n;<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>注意到在叠加多个FilterInputStream，我们只需要持有最外层的InputStream，并且，当最外层的InputStream关闭时（在<code>try(resource)</code>块的结束处自动关闭），内层的InputStream的close()方法也会被自动调用，并最终调用到最核心的“基础”InputStream，因此不存在资源泄露。</p>
<h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>Java的IO标准库使用Filter模式为InputStream和OutputStream增加功能：</p>
<ul>
<li>可以把一个InputStream和任意个FilterInputStream组合；</li>
<li>可以把一个OutputStream和任意个FilterOutputStream组合。</li>
</ul>
<p>Filter模式可以在运行期动态增加功能。</p>
<h2 id="操作Zip"><a href="#操作Zip" class="headerlink" title="操作Zip"></a>操作Zip</h2><p>ZipInputStream是一种FilterInputStream，它可以直接读取zip包的内容。另一个JarInputStream是从ZipInputStream派生，它增加的主要功能是直接读取jar文件里面的<code>MANIFEST.MF</code>文件。因为本质上jar包就是zip包，只是额外附加了一些固定的描述文件。</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs ascii">┌───────────────────┐<br>│    InputStream    │<br>└───────────────────┘<br>          ▲<br>          │<br>┌───────────────────┐<br>│ FilterInputStream │<br>└───────────────────┘<br>          ▲<br>          │<br>┌───────────────────┐<br>│InflaterInputStream│<br>└───────────────────┘<br>          ▲<br>          │<br>┌───────────────────┐<br>│  ZipInputStream   │<br>└───────────────────┘<br>          ▲<br>          │<br>┌───────────────────┐<br>│  JarInputStream   │<br>└───────────────────┘<br></code></pre></td></tr></tbody></table></figure>

<h3 id="读取zip包"><a href="#读取zip包" class="headerlink" title="读取zip包"></a>读取zip包</h3><p>我们来看看ZipInputStream的基本用法。我们要创建一个ZipInputStream，通常是传入一个FileInputStream作为数据源，然后，循环调用getNextEntry()，直到返回null，表示zip流结束。</p>
<p>一个ZipEntry表示一个压缩文件或目录，如果是压缩文件，我们就用read()方法不断读取，直到返回-1：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">try</span> (ZipInputStream zip = <span class="hljs-keyword">new</span> ZipInputStream(<span class="hljs-keyword">new</span> FileInputStream(...))) {<br>    ZipEntry entry = <span class="hljs-keyword">null</span>;<br>    <span class="hljs-keyword">while</span> ((entry = zip.getNextEntry()) != <span class="hljs-keyword">null</span>) {<br>        String name = entry.getName();<br>        <span class="hljs-keyword">if</span> (!entry.isDirectory()) {<br>            <span class="hljs-keyword">int</span> n;<br>            <span class="hljs-keyword">while</span> ((n = zip.read()) != -<span class="hljs-number">1</span>) {<br>                ...<br>            }<br>        }<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<h3 id="写入zip包"><a href="#写入zip包" class="headerlink" title="写入zip包"></a>写入zip包</h3><p>ZipOutputStream是一种FilterOutputStream，它可以直接写入内容到zip包，可以把多份数据写入zip包。我们要先创建一个ZipOutputStream，通常是包装一个FileOutputStream，然后，每写入一个文件前，先调用putNextEntry()，然后用write()写入byte[]数据，写入完毕后调用closeEntry()结束这个文件的打包。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">try</span> (ZipOutputStream zip = <span class="hljs-keyword">new</span> ZipOutputStream(<span class="hljs-keyword">new</span> FileOutputStream(...))) {<br>    File[] files = ...<br>    <span class="hljs-keyword">for</span> (File file : files) {<br>        zip.putNextEntry(<span class="hljs-keyword">new</span> ZipEntry(file.getName()));<br>        zip.write(getFileDataAsBytes(file));<br>        zip.closeEntry();<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>上面的代码没有考虑文件的目录结构。如果要实现目录层次结构，<code>new ZipEntry(name)</code>传入的name要用相对路径。</p>
<h2 id="读取classpath资源"><a href="#读取classpath资源" class="headerlink" title="读取classpath资源"></a>读取classpath资源</h2><p>很多Java程序启动的时候，都需要读取配置文件。例如，从一个<code>.properties</code>文件读取配置。这就需要在磁盘的某一目录创建相应的文件。Linux和Windows的路径又不一致。因此，从磁盘的固定目录读取配置文件，不是一个好办法。那有没有路径无关的读取文件方式呢？</p>
<p>我们知道，Java存放<code>.class</code>的目录或jar包也可以包含其他任意类型的文件，例如：</p>
<ul>
<li>配置文件，例如.properties；</li>
<li>图片文件，例如.jpg；</li>
<li>文本文件，例如.txt，.csv；</li>
<li>…</li>
</ul>
<p>从classpath读取文件就可以避免不同环境下路径文件不一致的问题。</p>
<p>在classpath中的资源文件，路径总是以<code>/</code>开头，我们先获取当前的<code>Class</code>对象，然后调用<code>getResourceAsStream()</code>就可以从classpath读取任意的资源文件。调用<code>getResourceAsStream()</code>需要特别注意的一点是，如果资源文件不存在，它将返回null。因此，我们需要检查返回的InputStream是否为null，如果为null，表示资源文件在classpath中没有找到：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">try</span> (InputStream input = getClass().getResourceAsStream(<span class="hljs-string">"/default.properties"</span>)) {<br>    <span class="hljs-keyword">if</span> (input != <span class="hljs-keyword">null</span>) {<br>        <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span></span><br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>如果我们把默认的配置放到jar包里，再从外部文件系统读取一个可选的配置文件，就可以做到既有默认的配置文件，又可以让用户自己修改配置。这样读取配置，应用程序的启动就更加灵活。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java">Properties props = <span class="hljs-keyword">new</span> Properties();<br>props.load(inputStreamFromClassPath(<span class="hljs-string">"/default.properties"</span>));<br>props.load(inputStreamFromFile(<span class="hljs-string">"./conf.properties"</span>));<br></code></pre></td></tr></tbody></table></figure>

<h2 id="序列化"><a href="#序列化" class="headerlink" title="序列化"></a>序列化</h2><p>序列化是指把一个Java对象变成二进制内容，本质上就是一个byte[]数组。为什么要把Java对象序列化？因为序列化后可以把byte[]保存到文件中，或者把byte[]通过网络传输到远程，这样，就相当于把Java对象存储到文件或者通过网络传输出去了。</p>
<p>有序列化就有反序列化，即把一个二进制内容（也就是byte[]数组）变回Java对象。有了反序列化，保存到文件中的byte[]数组又可以变回Java对象，或者从网络上读取byte[]并把它变为Java对象。</p>
<p>我们来看看如何把一个Java对象序列化。</p>
<p>一个Java对象要能序列化，必须实现一个特殊的<code>java.io.Serializable</code>接口，它的定义如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Serializable</span></span>{<br>    <br>}<br></code></pre></td></tr></tbody></table></figure>

<p>Serializable接口没有定义任何方法，它是一个空接口。我们把这样的空接口称为标记接口（Marker Interface），实现了标记接口的类仅仅是给自身贴了个标记，并没有增加任何方法。</p>
<h3 id="序列化-1"><a href="#序列化-1" class="headerlink" title="序列化"></a>序列化</h3><p>把一个Java对象变为byte[]数组，需要使用ObjectOutputStream。它负责把一个Java对象写入一个字节流：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> java.io.*;<br><span class="hljs-keyword">import</span> java.util.Arrays;<br><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Main</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>        ByteArrayOutputStream buffer = <span class="hljs-keyword">new</span> ByteArrayOutputStream();<br>        <span class="hljs-keyword">try</span> (ObjectOutputStream output = <span class="hljs-keyword">new</span> ObjectOutputStream(buffer)) {<br>            <span class="hljs-comment">// 写入int:</span><br>            output.writeInt(<span class="hljs-number">12345</span>);<br>            <span class="hljs-comment">// 写入String:</span><br>            output.writeUTF(<span class="hljs-string">"Hello"</span>);<br>            <span class="hljs-comment">// 写入Object:</span><br>            output.writeObject(Double.valueOf(<span class="hljs-number">123.456</span>));<br>        }<br>        System.out.println(Arrays.toString(buffer.toByteArray()));<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>ObjectOutputStream既可以写入基本类型，如int，boolean，也可以写入String（以UTF-8编码），还可以写入实现了Serializable接口的Object。因为写入Object时需要大量的类型信息，所以写入的内容很大。</p>
<h3 id="反序列化"><a href="#反序列化" class="headerlink" title="反序列化"></a>反序列化</h3><p>和ObjectOutputStream相反，ObjectInputStream负责从一个字节流读取Java对象：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">try</span> (ObjectInputStream input = <span class="hljs-keyword">new</span> ObjectInputStream(...)) {<br>    <span class="hljs-keyword">int</span> n = input.readInt();<br>    String s = input.readUTF();<br>    Double d = (Double) input.readObject();<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>除了能读取基本类型和String类型外，调用<code>readObject()</code>可以直接返回一个Object对象。要把它变成一个特定类型，必须强制转型。readObject()可能抛出的异常有：</p>
<ul>
<li>ClassNotFoundException，没有找到对应的Class</li>
<li>InvalidClassException，Class不匹配</li>
</ul>
<p>对于ClassNotFoundException，这种情况常见于一台电脑上的Java程序把一个Java对象，例如，Person对象序列化以后，通过网络传给另一台电脑上的另一个Java程序，但是这台电脑的Java程序并没有定义Person类，所以无法反序列化。</p>
<p>对于InvalidClassException，这种情况常见于序列化的Person对象定义了一个int类型的age字段，但是反序列化时，Person类定义的age字段被改成了long类型，所以导致class不兼容。</p>
<p>为了避免这种class定义变动导致的不兼容，Java的序列化允许class定义一个特殊的<code>serialVersionUID</code>静态变量，用于标识Java类的序列化“版本”，通常可以由IDE自动生成。如果增加或修改了字段，可以改变serialVersionUID的值，这样就能自动阻止不匹配的class版本。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Serializable</span> </span>{<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">long</span> serialVersionUID = <span class="hljs-number">2709425275741743919L</span>;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>反序列化时，由JVM直接构造出Java对象，不调用构造方法，构造方法内部的代码，在反序列化时根本不可能执行。</p>
<h3 id="安全性"><a href="#安全性" class="headerlink" title="安全性"></a>安全性</h3><p>因为Java的序列化机制可以导致一个实例能直接从byte[]数组创建，而不经过构造方法，因此，它存在一定的安全隐患。一个精心构造的byte[]数组被反序列化后可以执行特定的Java代码，从而导致严重的安全漏洞。</p>
<p>实际上，Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题，也存在兼容性问题。更好的序列化方法是通过JSON这样通用的数据结构来实现，只输出基本类型和String的内容，而不存储任何与代码相关的信息。</p>
<p>Java的序列化机制仅适用于Java，如果需要与其他语言交换数据，必须使用通用的序列化方法，例如JSON。</p>
<h2 id="Reader"><a href="#Reader" class="headerlink" title="Reader"></a>Reader</h2><p>Reader是Java的IO库提供的另一个输入流接口。和InputStream的区别是，InputStream是一个字节流，即以byte为单位读取，而Reader是一个字符流，即以char为单位读取。</p>
<p>java.io.Reader是所有字符输入流的超类，它最主要的方法是：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">read</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException</span>;<br></code></pre></td></tr></tbody></table></figure>

<p>这个方法读取字符流的下一个字符，并返回字符表示的int，范围是0~65535。如果已读到末尾，返回-1。</p>
<h3 id="FileReader"><a href="#FileReader" class="headerlink" title="FileReader"></a>FileReader</h3><p>FileReader是Reader的一个子类，它可以打开文件并获取Reader。下面的代码演示了如何完整地读取一个FileReader的所有字符：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">readFile</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>    <span class="hljs-comment">// 创建一个FileReader对象:</span><br>    Reader reader = <span class="hljs-keyword">new</span> FileReader(<span class="hljs-string">"src/readme.txt"</span>); <span class="hljs-comment">// 字符编码是???</span><br>    <span class="hljs-keyword">for</span> (;;) {<br>        <span class="hljs-keyword">int</span> n = reader.read(); <span class="hljs-comment">// 反复调用read()方法，直到返回-1</span><br>        <span class="hljs-keyword">if</span> (n == -<span class="hljs-number">1</span>) {<br>            <span class="hljs-keyword">break</span>;<br>        }<br>        System.out.println((<span class="hljs-keyword">char</span>)n); <span class="hljs-comment">// 打印char</span><br>    }<br>    reader.close(); <span class="hljs-comment">// 关闭流</span><br>}<br></code></pre></td></tr></tbody></table></figure>

<p>如果我们读取一个纯ASCII编码的文本文件，上述代码工作是没有问题的。但如果文件中包含中文，就会出现乱码，因为FileReader默认的编码与系统相关，例如，Windows系统的默认编码可能是GBK，打开一个UTF-8编码的文本文件就会出现乱码。</p>
<p>要避免乱码问题，我们需要在创建FileReader时指定编码：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">Reader reader = <span class="hljs-keyword">new</span> FileReader(<span class="hljs-string">"src/readme.txt"</span>, StandardCharsets.UTF_8);<br></code></pre></td></tr></tbody></table></figure>

<p>和InputStream一样，Reader也是一种资源，需要保证出错的时候也能正确关闭，所以我们需要用try(resource)来保证Reader在无论有没有IO错误的时候都能正确关闭：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">try</span> (Reader reader = <span class="hljs-keyword">new</span> FileReader(<span class="hljs-string">"src/readme.txt"</span>, StandardCharsets.UTF_8) {<br>    <span class="hljs-comment">// TODO</span><br>}<br></code></pre></td></tr></tbody></table></figure>

<p>Reader还提供了一次性读取若干字符并填充到char[]数组的方法：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">read</span><span class="hljs-params">(<span class="hljs-keyword">char</span>[] c)</span> <span class="hljs-keyword">throws</span> IOException</span><br></code></pre></td></tr></tbody></table></figure>

<p>它返回实际读入的字符个数，最大不超过char[]数组的长度，返回-1表示流结束。利用这个方法，我们可以先设置一个缓冲区，然后，每次尽可能地填充缓冲区：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">readFile</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> IOException </span>{<br>    <span class="hljs-keyword">try</span> (Reader reader = <span class="hljs-keyword">new</span> FileReader(<span class="hljs-string">"src/readme.txt"</span>, StandardCharsets.UTF_8)) {<br>        <span class="hljs-keyword">char</span>[] buffer = <span class="hljs-keyword">new</span> <span class="hljs-keyword">char</span>[<span class="hljs-number">1000</span>];<br>        <span class="hljs-keyword">int</span> n;<br>        <span class="hljs-keyword">while</span> ((n = reader.read(buffer)) != -<span class="hljs-number">1</span>) {<br>            System.out.println(<span class="hljs-string">"read "</span> + n + <span class="hljs-string">" chars."</span>);<br>        }<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<h3 id="CharArrayReader"><a href="#CharArrayReader" class="headerlink" title="CharArrayReader"></a>CharArrayReader</h3><p>CharArrayReader可以在内存中模拟一个Reader，它的作用实际上是把一个char[]数组变成一个Reader，这和ByteArrayInputStream非常类似：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">try</span> (Reader reader = <span class="hljs-keyword">new</span> CharArrayReader(<span class="hljs-string">"Hello"</span>.toCharArray())) {<br>}<br></code></pre></td></tr></tbody></table></figure>

<h3 id="StringReader"><a href="#StringReader" class="headerlink" title="StringReader"></a>StringReader</h3><p>StringReader可以直接把String作为数据源，它和CharArrayReader几乎一样：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">try</span> (Reader reader = <span class="hljs-keyword">new</span> StringReader(<span class="hljs-string">"Hello"</span>)) {<br>}<br></code></pre></td></tr></tbody></table></figure>

<h3 id="InputStreamReader"><a href="#InputStreamReader" class="headerlink" title="InputStreamReader"></a>InputStreamReader</h3><p>Reader和InputStream有什么关系？</p>
<p>除了特殊的CharArrayReader和StringReader，普通的Reader实际上是基于InputStream构造的，因为Reader需要从InputStream中读入字节流（byte），然后，根据编码设置，再转换为char就可以实现字符流。如果我们查看FileReader的源码，它在内部实际上持有一个FileInputStream。</p>
<p>既然Reader本质上是一个基于InputStream的byte到char的转换器，那么，如果我们已经有一个InputStream，想把它转换为Reader，是完全可行的。InputStreamReader就是这样一个转换器，它可以把任何InputStream转换为Reader。示例代码如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// 持有InputStream:</span><br>InputStream input = <span class="hljs-keyword">new</span> FileInputStream(<span class="hljs-string">"src/readme.txt"</span>);<br><span class="hljs-comment">// 变换为Reader:</span><br>Reader reader = <span class="hljs-keyword">new</span> InputStreamReader(input, <span class="hljs-string">"UTF-8"</span>);<br></code></pre></td></tr></tbody></table></figure>

<p>构造InputStreamReader时，我们需要传入InputStream，还需要指定编码，就可以得到一个Reader对象。上述代码可以通过try (resource)更简洁地改写如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">try</span> (Reader reader = <span class="hljs-keyword">new</span> InputStreamReader(<span class="hljs-keyword">new</span> FileInputStream(<span class="hljs-string">"src/readme.txt"</span>), <span class="hljs-string">"UTF-8"</span>)) {<br>    <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span></span><br>}<br></code></pre></td></tr></tbody></table></figure>

<p>上述代码实际上就是FileReader的一种实现方式。使用try (resource)结构时，当我们关闭Reader时，它会在内部自动调用InputStream的close()方法，所以，只需要关闭最外层的Reader对象即可。</p>
<h2 id="PrintStream和PrintWriter"><a href="#PrintStream和PrintWriter" class="headerlink" title="PrintStream和PrintWriter"></a>PrintStream和PrintWriter</h2><p>PrintStream是一种FilterOutputStream，它在OutputStream的接口上，额外提供了一些写入各种数据类型的方法。</p>
<ul>
<li>写入int：print(int)</li>
<li>写入boolean：print(boolean)</li>
<li>写入String：print(String)</li>
<li>写入Object：print(Object)，实际上相当于print(object.toString())</li>
<li>…</li>
</ul>
<p>以及对应的一组println()方法，它会自动加上换行符。我们经常使用的System.out.println()实际上就是使用PrintStream打印各种数据，其中System.out是系统默认提供的PrintStream，表示标准输出。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java">System.out.print(<span class="hljs-number">12345</span>); <span class="hljs-comment">// 输出12345</span><br>System.out.print(<span class="hljs-keyword">new</span> Object()); <span class="hljs-comment">// 输出类似java.lang.Object@3c7a835a</span><br>System.out.println(<span class="hljs-string">"Hello"</span>); <span class="hljs-comment">// 输出Hello并换行</span><br></code></pre></td></tr></tbody></table></figure>

<p>System.err是系统默认提供的标准错误输出。</p>
<p>PrintStream和OutputStream相比，除了添加了一组print()/println()方法，可以打印各种数据类型。此外，它还有一个额外优点，就是不会抛出IOException，这样我们在编写代码时，就不必捕获IOException。</p>
<h3 id="PrintWriter"><a href="#PrintWriter" class="headerlink" title="PrintWriter"></a>PrintWriter</h3><p>PrintStream输出的总是byte数据，而PrintWriter则是扩展了Writer接口，它的print()/println()方法总是输出char数据，两者的使用方法几乎一模一样。</p>
<h2 id="使用Files"><a href="#使用Files" class="headerlink" title="使用Files"></a>使用Files</h2><p>从Java 7开始，提供了Files和Paths这两个工具类，能极大的方便我们读写文件。虽然Files和Paths是java.nio包里面的类，但他俩封装了很多读写文件的简单方法。</p>
<p>例如，我们要把一个文件的全部内容读取为一个byte[]，可以这么写：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">byte</span>[] data = Files.readAllBytes(Paths.get(<span class="hljs-string">"/path/to/file.txt"</span>));<br></code></pre></td></tr></tbody></table></figure>

<p>如果是文本文件，可以把一个文件的所有内容全部读取为String：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// 默认使用UTF-8编码读取:</span><br>String content1 = Files.readString(Paths.get(<span class="hljs-string">"/path/to/file.txt"</span>));<br><span class="hljs-comment">// 可指定编码:</span><br>String content2 = Files.readString(Paths.get(<span class="hljs-string">"/path/to/file.txt"</span>), StandardCharsets.ISO_8859_1);<br><span class="hljs-comment">// 按行读取并返回每行内容:</span><br>List&lt;String&gt; lines = Files.readAllLines(Paths.get(<span class="hljs-string">"/path/to/file.txt"</span>));<br></code></pre></td></tr></tbody></table></figure>

<p>写入文件也非常方便：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// 写入二进制文件:</span><br><span class="hljs-keyword">byte</span>[] data = ...<br>Files.write(Paths.get(<span class="hljs-string">"/path/to/file.txt"</span>), data);<br><span class="hljs-comment">// 写入文本并指定编码:</span><br>Files.writeString(Paths.get(<span class="hljs-string">"/path/to/file.txt"</span>), <span class="hljs-string">"文本内容..."</span>, StandardCharsets.ISO_8859_1);<br><span class="hljs-comment">// 按行写入文本:</span><br>List&lt;String&gt; lines = ...<br>Files.write(Paths.get(<span class="hljs-string">"/path/to/file.txt"</span>), lines);<br></code></pre></td></tr></tbody></table></figure>

<p>此外，Files工具类还有copy()，delete()，exists()，move()等快捷方法操作文件和目录。</p>
<p>最后要特别注意的是，Files提供的读写方法，受内存限制，只能读写小文件，例如配置文件等，不可一次读如几个G的大文件。读写大型文件仍然要使用文件流，每次只读写一部分文件内容。</p>
</body></html>
    
    </div>
    
    
    <div class="columns is-mobile is-multiline article-nav">
        <span class="column is-12-mobile is-half-desktop  article-nav-prev">
            
            <a href="/Gallery/zh-cn/gallery/">My Gallery Test</a>
            
        </span>
        <span class="column is-12-mobile is-half-desktop  article-nav-next">
            
            <a href="/Study/Java/zh-cn/%E9%9B%86%E5%90%88/">集合</a>
            
        </span>
    </div>
    
</article>


<div class="sharebox">
    
<div class="sharethis-inline-share-buttons"></div>
<script type='text/javascript' src='//platform-api.sharethis.com/js/sharethis.js#property=608c1408daac690012507aa2&amp;product=sop' async='async'></script>

</div>



    </div>
</section>
    <footer class="footer">
    <div class="container">
        <div class="columns content">
            <div class="column is-narrow has-text-centered">
                &copy; 2021 wanzixin&nbsp;
                Powered by <a href="http://hexo.io/" target="_blank">Hexo</a> & <a
                        target="_blank" rel="noopener" href="http://github.com/ppoffice/hexo-theme-minos">Minos</a>
            </div>
            <div class="column is-hidden-mobile"></div>

            
            <div class="column is-narrow">
                <div class="columns is-mobile is-multiline is-centered">
                
                    
                <a class="column is-narrow has-text-black" title="GitHub" target="_blank" rel="noopener" href="https://github.com/ppoffice/hexo-theme-minos">
                    
                    GitHub
                    
                </a>
                
                </div>
            </div>
            
            
<div class="column is-narrow has-text-centered">
    <div class="dropdown is-up is-right is-hoverable" style="margin-top: -0.2em;">
        <div class="dropdown-trigger">
            <button class="button is-small" aria-haspopup="true" aria-controls="dropdown-menu7">
                <span class="icon">
                    <i class="fas fa-globe"></i>
                </span>
                <span>简体中文</span>
                <span class="icon is-small">
            <i class="fas fa-angle-down" aria-hidden="true"></i>
          </span>
            </button>
        </div>
        <div class="dropdown-menu has-text-left" role="menu">
            <div class="dropdown-content">
            
                <a href="/Study/Java/zh-cn/IO/" class="dropdown-item">
                    English
                </a>
            
                <a href="/zh-cn/Study/Java/zh-cn/IO/" class="dropdown-item">
                    简体中文
                </a>
            
            </div>
        </div>
    </div>
</div>

        </div>
    </div>
</footer>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment-with-locales.min.js"></script>

<!-- test if the browser is outdated -->
<div id="outdated">
    <h6>Your browser is out-of-date!</h6>
    <p>Update your browser to view this website correctly. <a id="btnUpdateBrowser" target="_blank" rel="noopener" href="http://outdatedbrowser.com/">Update my browser now </a></p>
    <p class="last"><a href="#" id="btnCloseUpdateBrowser" title="Close">&times;</a></p>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/outdated-browser/1.1.5/outdatedbrowser.min.js"></script>
<script>
    $(document).ready(function () {
        // plugin function, place inside DOM ready function
        outdatedBrowser({
            bgColor: '#f25648',
            color: '#ffffff',
            lowerThan: 'flex'
        })
    });
</script>

<script>
    window.FontAwesomeConfig = {
        searchPseudoElements: true
    }
    moment.locale("zh-CN");
</script>


    
    
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script>
    MathJax.Hub.Config({
        "HTML-CSS": {
            matchFontHeight: false
        },
        SVG: {
            matchFontHeight: false
        },
        CommonHTML: {
            matchFontHeight: false
        },
        tex2jax: {
            inlineMath: [
                ['$','$'],
                ['\\(','\\)']
            ]
        }
    });
</script>

    
    
    
    
<script src="//cdnjs.cloudflare.com/ajax/libs/lightgallery/1.6.8/js/lightgallery-all.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.6.5/js/jquery.justifiedGallery.min.js"></script>
<script>
    (function ($) {
        $(document).ready(function () {
            if (typeof($.fn.lightGallery) === 'function') {
                $('.article.gallery').lightGallery({ selector: '.gallery-item' });
            }
            if (typeof($.fn.justifiedGallery) === 'function') {
                $('.justified-gallery').justifiedGallery();
            }
        });
    })(jQuery);
</script>

    
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js"></script>
    <style>
        .hljs {
            position: relative;
        }

        .hljs .clipboard-btn {
            float: right;
            color: #9a9a9a;
            background: none;
            border: none;
            cursor: pointer;
        }

        .hljs .clipboard-btn:hover {
          color: #8a8a8a;
        }

        .hljs > .clipboard-btn {
            display: none;
            position: absolute;
            right: 4px;
            top: 4px;
        }

        .hljs:hover > .clipboard-btn {
            display: inline;
        }

        .hljs > figcaption > .clipboard-btn {
            margin-right: 4px;
        }
    </style>
    <script>
      $(document).ready(function () {
        $('figure.hljs').each(function(i, figure) {
          var codeId = 'code-' + i;
          var code = figure.querySelector('.code');
          var copyButton = $('<button>Copy <i class="far fa-clipboard"></i></button>');
          code.id = codeId;
          copyButton.addClass('clipboard-btn');
          copyButton.attr('data-clipboard-target-id', codeId);

          var figcaption = figure.querySelector('figcaption');

          if (figcaption) {
            figcaption.append(copyButton[0]);
          } else {
            figure.prepend(copyButton[0]);
          }
        })

        var clipboard = new ClipboardJS('.clipboard-btn', {
          target: function(trigger) {
            return document.getElementById(trigger.getAttribute('data-clipboard-target-id'));
          }
        });
        clipboard.on('success', function(e) {
          e.clearSelection();
        })
      })
    </script>

    
    

    



<script src="/js/script.js"></script>


    
    <div class="searchbox ins-search">
    <div class="searchbox-mask"></div>
    <div class="searchbox-container ins-search-container">
        <div class="searchbox-input-wrapper">
            <input type="text" class="searchbox-input ins-search-input" placeholder="站内搜索" />
            <span class="searchbox-close ins-close ins-selectable"><i class="fa fa-times-circle"></i></span>
        </div>
        <div class="searchbox-result-wrapper ins-section-wrapper">
            <div class="ins-section-container"></div>
        </div>
    </div>
</div>
<script>
    (function (window) {
        var INSIGHT_CONFIG = {
            TRANSLATION: {
                POSTS: '文章',
                PAGES: '页面',
                CATEGORIES: '分类',
                TAGS: '标签',
                UNTITLED: '(无标题)',
            },
            CONTENT_URL: '/content.zh-cn.json',
        };
        window.INSIGHT_CONFIG = INSIGHT_CONFIG;
    })(window);
</script>

<script src="/js/insight.js"></script>

    
</body>
</html>